Provider Components
Video Summary
Let's suppose we have 3 different pieces of state we want to pass through context. We set it up with 3 different context providers, like so:
export const UserContext = React.createContext();export const ThemeContext = React.createContext();export const PlaybackRateContext = React.createContext();
function App() { const [user, setUser] = React.useState(null); const [theme, setTheme] = React.useState('light'); const [playbackRate, setPlaybackRate] = React.useState(1);
return ( <UserContext.Provider value={user}> <ThemeContext.Provider value={theme}> <PlaybackRateContext.Provider value={playbackRate}> <Homepage /> </PlaybackRateContext.Provider> </ThemeContext.Provider> </UserContext.Provider> );}
It often surprises developers that we'd want to use multiple contexts. Wouldn't it be simpler to group them all together, like this?
export const AppContext = React.createContext();
function App() { const [user, setUser] = React.useState(null); const [theme, setTheme] = React.useState('light'); const [playbackRate, setPlaybackRate] = React.useState(1);
return ( <AppContext.Provider value={{ user, theme, playbackRate }}> <Homepage /> </AppContext.Provider> );}
While this might look simpler at first glance, there are two reasons why it's an established best practice to create individual contexts for each discrete concern:
- There are potential performance benefits (we'll learn more about this later in the course).
- It can improve code readability.
That second point might be surprising: after all, the code seems more readable with a single AppContext
, doesn't it?
Well, here's the thing: this example is wildly unrealistic. In reality, each concern often has a bunch of other stuff.
Let's look at a more realistic example:
import React from 'react';import useSWR from 'swr';
import { COLORS } from './constants';import Homepage from './Homepage';
export const UserContext = React.createContext();export const ThemeContext = React.createContext();export const PlaybackRateContext = React.createContext();
const ENDPOINT = 'https://jor-test-api.vercel.app/api/get-current-user';
async function fetcher(endpoint) { const response = await fetch(endpoint); const json = await response.json();
if (!json.ok) { throw json; }
return json.user;}
function App() { const { data: user, error: userError, mutate: mutateUser, } = useSWR(ENDPOINT, fetcher);
const [theme, setTheme] = React.useState(() => { return window.localStorage.getItem('color-theme') || 'light'; });
const [playbackRate, setPlaybackRate] = React.useState(1);
React.useEffect(() => { window.localStorage.setItem('color-theme', theme); }, [theme]);
const toggleTheme = React.useCallback(() => { setTheme((currentTheme) => { return currentTheme === 'light' ? 'dark' : 'light'; }); }, []);
const colors = COLORS[theme];
const logOut = React.useCallback(() => { mutateUser({ user: null, }); }, [mutateUser]);
const editProfile = React.useCallback((newData) => { mutateUser({ user: { ...user, ...newData }, }); }, [user, mutateUser]);
const resetPlaybackRate = React.useCallback(() => { setPlaybackRate(1); }, []);
return ( <UserContext.Provider value={{ user, logOut, editProfile }}> <ThemeContext.Provider value={{ theme, toggleTheme, colors, }}> <PlaybackRateContext.Provider value={{ playbackRate, setPlaybackRate, resetPlaybackRate, }} > <Homepage /> </PlaybackRateContext.Provider> </ThemeContext.Provider> </UserContext.Provider> );}
export default App;
This is a lot to take in, but essentially, each of our 3 concerns has additional stuff:
- The
user
object is now being fetched with SWR, like we learned in the last module. We also have some helper functions for common actions, like logging out or editing the user - The
theme
state is being persisted in localStorage, like we saw in the Effect exercises. It also has a derivedcolors
variable, and atoggleTheme
helper function - The
playbackRate
state has aresetPlaybackRate
helper function.
As a result, App.js
feels really cluttered, and it's hard to understand what's going on here.
To improve this situation, we're going to use the provider component pattern.
First, we'll create a new component, UserProvider
, in a new file. This component will manage everything related to the “user” concern.
Everything related to the user gets moved over:
import React from 'react';import useSWR from 'swr';
export const UserContext = React.createContext();
const ENDPOINT = 'https://jor-test-api.vercel.app/api/get-current-user';
async function fetcher(endpoint) { const response = await fetch(endpoint); const json = await response.json();
if (!json.ok) { throw json; }
return json.user;}
function UserProvider({ children }) { const { data: user, error: userError, mutate: mutateUser } = useSWR( ENDPOINT, fetcher );
const logOut = React.useCallback(() => { mutateUser({ user: null, }); }, [mutateUser]);
const editProfile = React.useCallback( (newData) => { mutateUser({ user: { ...user, ...newData, }, }); }, [user, mutateUser] );
return ( <UserContext.Provider value={{ user, logOut, editProfile }} > {children} </UserContext.Provider> );}
export default UserProvider;
We'll export the context itself as a named export, and the component as the default export.
This component is ultimately a wrapper around UserContext.Provider
, a component we get from React (by calling React.createContext()
).
Inside App
, we'd use UserProvider
in the same spot we'd have used UserContext.Provider
:
// App.jsfunction App() { // Theme and playbackRate stuff unchanged
return ( <UserProvider> <ThemeContext.Provider value={{ theme, toggleTheme, colors, }}> <PlaybackRateContext.Provider value={{ playbackRate, setPlaybackRate, resetPlaybackRate, }} > <Homepage /> </PlaybackRateContext.Provider> </ThemeContext.Provider> </UserProvider> );}
We also need to update the components that consume the context, so that we're updating it from this new file:
// Homepage.jsimport { UserContext } from './UserProvider';
There are two benefits to using this pattern:
- Everything related to the
user
concern—including the state, the data-fetching, and the context—is grouped in 1 spot. - The
App
component is decluttered, letting us quickly understand how the application is structured, without drowning in the details.
Here's how our App
component would look if we created provider components for all 3 concerns:
import Homepage from './Homepage';import UserProvider from './UserProvider';import ThemeProvider from './ThemeProvider';import PlaybackRateProvider from './PlaybackRateProvider';
function App() { return ( <UserProvider> <ThemeProvider> <PlaybackRateProvider> <Homepage /> </PlaybackRateProvider> </ThemeProvider> </UserProvider> );}
This pattern can be difficult to parse, but hopefully it'll make more sense once you try it for yourself, below!
Practice - extracting two more provider components
The sandbox below picks up where the video left off. The UserProvider
component has been created, and it's up to you to create two new provider components: ThemeProvider
and PlaybackRateProvider
.
Acceptance Criteria:
- The
ThemeProvider
component should manage everything related to thetheme
state and context. - The
PlaybackRateProvider
component should manage everything related to theplaybackRate
state and context. App.js
should import and use these two new components, matching the style/format of theUserProvider
component.- Inside
Homepage.js
, the imports should be updated, so that we're importing the contexts from the provider components, not fromApp
Code Playground